using System;
using System.Collections.Generic;
using System.Linq;

namespace Agatha.Common
{
	public interface IRequestDispatcher : IDisposable
	{
		IEnumerable<Response> Responses { get; }

		void Add(Request request);
		void Add(params Request[] requestsToAdd);
		void Add(string key, Request request);
		void Add<TRequest>(Action<TRequest> action) where TRequest : Request, new();
		void Send(params OneWayRequest[] oneWayRequests);
		bool HasResponse<TResponse>() where TResponse : Response;
		TResponse Get<TResponse>() where TResponse : Response;
		TResponse Get<TResponse>(string key) where TResponse : Response;
		TResponse Get<TResponse>(Request request) where TResponse : Response;
		void Clear();
	}

	public class RequestDispatcher : Disposable, IRequestDispatcher
	{
		private readonly IRequestProcessor requestProcessor;

		private Dictionary<string, Type> keyToTypes;
		protected Dictionary<string, int> keyToResultPositions;
		private List<Request> requests;
		private Response[] responses;

		public RequestDispatcher(IRequestProcessor requestProcessor)
		{
			this.requestProcessor = requestProcessor;
			InitializeState();
		}

		private void InitializeState()
		{
			requests = new List<Request>();
			responses = null;
			keyToTypes = new Dictionary<string, Type>();
			keyToResultPositions = new Dictionary<string, int>();
		}

		public IEnumerable<Request> SentRequests
		{
			get { return requests; }
		}

		public IEnumerable<Response> Responses
		{
			get
			{
				SendRequestsIfNecessary();
				return responses;
			}
		}

		public virtual void Add(params Request[] requestsToAdd)
		{
			foreach (var request in requestsToAdd)
			{
				Add(request);
			}
		}

		public virtual void Add<TRequest>(Action<TRequest> action) where TRequest : Request, new()
		{
			var request = new TRequest();
			action(request);
			Add(request);
		}

		public virtual void Add(Request request)
		{
			AddRequest(request, false);
		}

		public virtual void Add(string key, Request request)
		{
			AddRequest(request, true);
			keyToTypes[key] = request.GetType();
			keyToResultPositions[key] = requests.Count - 1;
		}

		public virtual void Send(params OneWayRequest[] oneWayRequests)
		{
			BeforeSendingRequests(oneWayRequests);
			requestProcessor.ProcessOneWayRequests(oneWayRequests);
		}

		public virtual bool HasResponse<TResponse>() where TResponse : Response
		{
			SendRequestsIfNecessary();
			return responses.OfType<TResponse>().Count() > 0;
		}

		public virtual TResponse Get<TResponse>() where TResponse : Response
		{
			SendRequestsIfNecessary();
			return responses.OfType<TResponse>().Single();
		}

		public virtual TResponse Get<TResponse>(string key) where TResponse : Response
		{
			SendRequestsIfNecessary();
			return (TResponse)responses[keyToResultPositions[key]];
		}

		public virtual TResponse Get<TResponse>(Request request) where TResponse : Response
		{
			Add(request);
			return Get<TResponse>();
		}

		public virtual void Clear()
		{
			InitializeState();
		}

		protected override void DisposeManagedResources()
		{
			if (requestProcessor != null) requestProcessor.Dispose();
		}

		protected virtual Response[] GetResponses(params Request[] requestsToProcess)
		{
			BeforeSendingRequests(requestsToProcess);
			return requestProcessor.Process(requestsToProcess);
		}

		protected virtual void BeforeSendingRequests(IEnumerable<Request> requestsToProcess) { }

		private void SendRequestsIfNecessary()
		{
			if (responses == null)
			{
				responses = GetResponses(requests.ToArray());
				DealWithPossibleExceptions(responses);
			}
		}

		private void DealWithPossibleExceptions(IEnumerable<Response> responsesToCheck)
		{
			foreach (var response in responsesToCheck)
			{
				if (response.ExceptionType == ExceptionType.Security)
				{
					DealWithSecurityException(response.Exception);
				}

				if (response.ExceptionType == ExceptionType.Unknown)
				{
					DealWithUnknownException(response.Exception);
				}
			}
		}

		protected virtual void DealWithUnknownException(ExceptionInfo exception) { }

		protected virtual void DealWithSecurityException(ExceptionInfo exceptionDetail) { }

		private void AddRequest(Request request, bool wasAddedWithKey)
		{
			Type requestType = request.GetType();

			if (RequestTypeIsAlreadyPresent(requestType) &&
				(RequestTypeIsNotAssociatedWithKey(requestType) || !wasAddedWithKey))
			{
				throw new InvalidOperationException(String.Format("A request of type {0} has already been added. "
																  + "Please add requests of the same type with a different key.", requestType.FullName));
			}

			requests.Add(request);
		}

		private bool RequestTypeIsNotAssociatedWithKey(Type requestType)
		{
			return !keyToTypes.Values.Contains(requestType);
		}

		private bool RequestTypeIsAlreadyPresent(Type requestType)
		{
			return requests.Count(r => r.GetType().Equals(requestType)) > 0;
		}
	}
}